package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"regexp"
	"strings"
	"sync"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)

var (
	objectHeaderKeys = []string{
		"Content-Length",
		"Content-Type",
		"ETag",
		"Last-Modified",
		"Content-MD5",
		"Access-Control-Allow-Origin",
		"Access-Control-Allow-Methods",
		"Access-Control-Allow-Age",
		"Access-Control-Allow-Headers",
		"Access-Control-Expose-Headers",
		"Cache-Control",
		"Content-Disposition",
		"Content-Encoding",
		"Expires",
	}
)

func copyObjectHeaders(w http.ResponseWriter, result *oss.ResultCommon) {
	header := w.Header()
	for _, key := range objectHeaderKeys {
		v, ok := result.Headers[key]
		if !ok {
			continue
		}
		header.Set(key, v[0])
	}
}

type Host struct {
	sslRedirect    bool
	ossClient      *oss.Client
	bucket         string
	prefix         string
	indexPage      string
	notFoundPage   string
	rewriteToIndex *regexp.Regexp
	rewriteOnMatch bool
}

func (h *Host) getObjectKey(r *http.Request) string {
	relPath := r.URL.Path
	if h.indexPage != "" {
		if relPath == "/" || h.rewriteToIndex != nil && h.rewriteToIndex.MatchString(relPath) == h.rewriteOnMatch {
			relPath = h.indexPage
		}
	}
	return h.prefix + relPath
}

func (h *Host) getNotFoundPage(_ *http.Request) string {
	return h.notFoundPage
}

type Server struct {
	hosts       map[string]*Host
	notFound    http.HandlerFunc
	certManager *CertManager
	maxBodySize int64
	listenHTTP  string
	listenHTTPS string
}

func (s *Server) handleHeadObject(h *Host, w http.ResponseWriter, r *http.Request) {
	objectKey := h.getObjectKey(r)
	result, err := h.ossClient.HeadObject(r.Context(), &oss.HeadObjectRequest{
		Bucket: oss.Ptr(h.bucket),
		Key:    oss.Ptr(objectKey),
	})
	if err != nil {
		slog.Error("head object", "bucket", h.bucket, "key", objectKey, "err", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if result.StatusCode != 200 {
		http.Error(w, result.Status, result.StatusCode)
		return
	}

	copyObjectHeaders(w, &result.ResultCommon)
	w.WriteHeader(http.StatusOK)
}

func (s *Server) handleGetObject(h *Host, w http.ResponseWriter, r *http.Request) {
	objectKey := h.getObjectKey(r)
	result, err := h.ossClient.GetObject(r.Context(), &oss.GetObjectRequest{
		Bucket: oss.Ptr(h.bucket),
		Key:    oss.Ptr(objectKey),
	})
	if err != nil {
		slog.Error("get object", "bucket", h.bucket, "key", objectKey, "err", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if result.StatusCode == 404 {
		objectKey = h.getNotFoundPage(r)
		result, err = h.ossClient.GetObject(r.Context(), &oss.GetObjectRequest{
			Bucket: oss.Ptr(h.bucket),
			Key:    oss.Ptr(objectKey),
		})
		if err != nil {
			slog.Error("get notFound object", "bucket", h.bucket, "key", objectKey, "err", err)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	if result.StatusCode != 200 {
		http.Error(w, result.Status, result.StatusCode)
		return
	}

	copyObjectHeaders(w, &result.ResultCommon)
	io.Copy(w, result.Body)
	result.Body.Close()
}

func (s *Server) handleHostRequest(h *Host, w http.ResponseWriter, r *http.Request) {
	if r.URL.Scheme == "http" && h.sslRedirect {
		r.URL.Scheme = "https"
		http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
		return
	}

	switch r.Method {
	case http.MethodHead:
		s.handleHeadObject(h, w, r)
	case http.MethodGet:
		s.handleGetObject(h, w, r)
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h := s.hosts[r.Host]
	if h == nil {
		s.notFound(w, r)
		return
	}

	s.handleHostRequest(h, w, r)
}

func (s *Server) listenAndServe() error {
	hsrv := &http.Server{
		Handler: s,
		TLSConfig: &tls.Config{
			GetCertificate: s.certManager.GetCertificate,
		},
	}
	defer hsrv.Shutdown(context.Background())

	var wg sync.WaitGroup

	if s.listenHTTP != "" {
		ln, err := net.Listen("tcp", s.listenHTTP)
		if err != nil {
			return err
		}

		wg.Add(1)
		go func() {
			defer wg.Done()
			err := hsrv.Serve(ln)
			if err != nil && err != http.ErrServerClosed {
				slog.Error("http serve", "err", err)
			}
		}()
	}

	if s.listenHTTPS != "" {
		ln, err := net.Listen("tcp", s.listenHTTPS)
		if err != nil {
			return err
		}

		wg.Add(1)
		go func() {
			defer wg.Done()
			err := hsrv.ServeTLS(ln, "", "")
			if err != nil && err != http.ErrServerClosed {
				slog.Error("http serve tls", "err", err)
			}
		}()
	}

	wg.Wait()

	return nil
}

func buildServerFromConfig(config *Config) (*Server, error) {
	ossClients := make(map[string]*oss.Client)
	for _, ossConfig := range config.Oss {
		providerConfig := ossConfig.Provider
		var provider credentials.CredentialsProvider
		switch providerConfig.Kind {
		case "ecsrole":
			provider = credentials.NewEcsRoleCredentialsProvider(credentials.EcsRamRole(providerConfig.RamRole))
		case "ecsrole_norefresh":
			provider = credentials.NewEcsRoleCredentialsProviderWithoutRefresh(credentials.EcsRamRole(providerConfig.RamRole))
		case "process":
			provider = credentials.NewProcessCredentialsProvider(providerConfig.Command)
		default:
			provider = credentials.NewStaticCredentialsProvider(providerConfig.AccessKeyID, providerConfig.AccessKeySecret, providerConfig.SecurityToken)
		}
		cfg := oss.LoadDefaultConfig().
			WithCredentialsProvider(provider).
			WithRegion(ossConfig.Region).
			WithDisableSSL(ossConfig.DisableSsl).
			WithUseInternalEndpoint(ossConfig.UseInternalEndpoint)
		ossClients[ossConfig.Name] = oss.NewClient(cfg)
	}

	hosts := make(map[string]*Host, len(config.Servers))
	for _, serverConfig := range config.Servers {
		ossClient := ossClients[serverConfig.Oss]
		if ossClient == nil {
			return nil, fmt.Errorf("no such oss client %s", serverConfig.Oss)
		}

		var (
			rewriteToIndex    *regexp.Regexp
			rewriteMismatched bool
		)
		if serverConfig.RewriteToIndex != nil {
			reRewrite := *serverConfig.RewriteToIndex
			reRewrite, rewriteMismatched = strings.CutPrefix(reRewrite, "!")
			re, err := regexp.Compile(reRewrite)
			if err != nil {
				return nil, fmt.Errorf("invalid regexp: %s", *serverConfig.RewriteToIndex)
			}
			rewriteToIndex = re
		}
		host := &Host{
			sslRedirect:    serverConfig.SslRedirect,
			ossClient:      ossClient,
			bucket:         serverConfig.Bucket,
			prefix:         serverConfig.Prefix,
			indexPage:      serverConfig.Index,
			notFoundPage:   serverConfig.Prefix + serverConfig.NotFound,
			rewriteToIndex: rewriteToIndex,
			rewriteOnMatch: !rewriteMismatched,
		}
		hosts[serverConfig.Host] = host
	}

	certManager, err := buildCertManager(config)
	if err != nil {
		return nil, err
	}

	return &Server{
		hosts:       hosts,
		notFound:    http.NotFound,
		certManager: certManager,
		listenHTTP:  config.Http.Listen,
		listenHTTPS: config.Http.ListenTLS,
	}, nil
}

type CertManager struct {
	hostCerts map[string]*tls.Certificate
}

func (m *CertManager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	slog.Debug("GetCertificate", "serverName", hello.ServerName)

	cert := m.hostCerts[hello.ServerName]
	if cert == nil {
		return nil, fmt.Errorf("no matched certificate %s", hello.ServerName)
	}

	return cert, nil
}

func buildCertManager(config *Config) (*CertManager, error) {
	certs := make(map[string]*tls.Certificate)
	for _, certConfig := range config.Http.Certs {
		cert, err := tls.LoadX509KeyPair(certConfig.Cert, certConfig.Key)
		if err != nil {
			return nil, err
		}
		certs[certConfig.Name] = &cert
	}
	hostCerts := make(map[string]*tls.Certificate)
	for _, serverConfig := range config.Servers {
		cert := certs[serverConfig.Cert]
		if cert == nil {
			return nil, fmt.Errorf("no such certificate %s", serverConfig.Cert)
		}
		hostCerts[serverConfig.Host] = cert
	}
	return &CertManager{
		hostCerts: hostCerts,
	}, nil
}
